package jmine.tec.test.utils.spring;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.log4j.Logger;
import org.hibernate.Criteria;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.criterion.Example;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.orm.hibernate3.SessionHolder;
import org.springframework.transaction.support.TransactionSynchronizationManager;

/**
 * Post Processor that watches for beans being created and saves them if the related bean name has been listed for save upon creation of
 * this processor. Current version works for non-singleton beans. Also, if an error occurs while saving some bean the hibernate session
 * cannot be flushed (as usual).
 * 
 * @author julien
 */
public class BeanSaverPostProcessor implements BeanPostProcessor {

    private Set<String> beansToBeSaved = new HashSet<String>();

    private final Logger logger = Logger.getLogger(BeanSaverPostProcessor.class);

    private SessionFactory sessionFactory;

    /**
     * Runs after the bean has been initialzed.
     * 
     * @param bean Object
     * @param beanName String
     * @return Object
     */
    public Object postProcessAfterInitialization(final Object bean, final String beanName) {
        if (this.beansToBeSaved.contains(beanName)) {
            try {
                Object savedBean = this.findSavedBean(bean);
                if (savedBean == null) {
                    savedBean = this.saveBean(bean);
                }
                return savedBean;
            } catch (Exception ex) {
                String msg = "Exception while saving bean '" + beanName + "'";
                this.logger.warn(msg, ex);
                throw new BeanInitializationException(msg, ex);
            }
        }
        return bean;
    }

    /**
     * Salva este Bean e o retorna.
     * 
     * @param bean Object
     * @return Object
     */
    private Object saveBean(final Object bean) {
        Session s = this.getSession();
        if (s == null) {
            s = this.sessionFactory.openSession();
            this.saveBean(bean, s);
            s.close();
        } else {
            this.saveBean(bean, s);
        }
        return bean;
    }

    /**
     * Salva este bean nesta session.
     * 
     * @param bean Object
     * @param session Session
     * @return Object
     */
    private Object saveBean(final Object bean, final Session session) {
        session.save(bean);
        return bean;
    }

    /**
     * Acha um Bean salvo equivalente a este.
     * 
     * @param bean Object
     * @return Object
     */
    private Object findSavedBean(final Object bean) {
        Session s = this.getSession();
        Object savedBean = null;
        if (s == null) {
            s = this.sessionFactory.openSession();
            savedBean = this.findSavedBean(bean, s);
            s.close();
        } else {
            savedBean = this.findSavedBean(bean, s);
        }
        return savedBean;
    }

    /**
     * Acha o bean salvo equivalente a este nesta session
     * 
     * @param bean Object
     * @param s Session
     * @return Object
     */
    private Object findSavedBean(final Object bean, final Session s) {
        Criteria executableCriteria = s.createCriteria(bean.getClass());
        executableCriteria.add(Example.create(bean));
        List<?> l = executableCriteria.list();
        return (!l.isEmpty()) ? l.get(0) : null;
    }

    /**
     * Acha uma Session nesta Thread se houver alguma. Senao volta null.
     * 
     * @return Session
     */
    private Session getSession() {
        SessionHolder resource = (SessionHolder) TransactionSynchronizationManager.getResource(this.sessionFactory);
        if (resource != null) {
            Session s = resource.getSession();
            if (s == null) {
                s = resource.getAnySession();
            }
            return s;
        }
        return null;
    }

    /**
     * Runs before the bean is initialzed.
     * 
     * @param bean Object
     * @param beanName String
     * @return Object
     */
    public Object postProcessBeforeInitialization(final Object bean, final String beanName) {
        return bean;
    }

    /**
     * @return the beansToBeSaved
     */
    public Set<String> getBeansToBeSaved() {
        return this.beansToBeSaved;
    }

    /**
     * @param beansToBeSaved the beansToBeSaved to set
     */
    public void setBeansToBeSaved(final Set<String> beansToBeSaved) {
        this.beansToBeSaved = beansToBeSaved;
    }

    /**
     * @return the sessionFactory
     */
    public SessionFactory getSessionFactory() {
        return this.sessionFactory;
    }

    /**
     * @param sessionFactory the sessionFactory to set
     */
    public void setSessionFactory(final SessionFactory sessionFactory) {
        this.sessionFactory = sessionFactory;
    }

}
